/* @minVersion 7.21.0 */
/* @onlyBabel7 */

import checkInRHS from "checkInRHS";
import setFunctionName from "setFunctionName";
import toPropertyKey from "toPropertyKey";

/**
  Enums are used in this file, but not assigned to vars to avoid non-hoistable values

  CONSTRUCTOR = 0;
  PUBLIC = 1;
  PRIVATE = 2;

  FIELD = 0;
  ACCESSOR = 1;
  METHOD = 2;
  GETTER = 3;
  SETTER = 4;

  STATIC = 5;

  CLASS = 10; // only used in assertValidReturnValue
*/

function applyDecs2301Factory() {
  function createAddInitializerMethod(initializers, decoratorFinishedRef) {
    return function addInitializer(initializer) {
      assertNotFinished(decoratorFinishedRef, "addInitializer");
      assertCallable(initializer, "An initializer");
      initializers.push(initializer);
    };
  }

  function assertInstanceIfPrivate(has, target) {
    if (!has(target)) {
      throw new TypeError(
        "Attempted to access private element on non-instance",
      );
    }
  }

  function memberDec(
    dec,
    name,
    desc,
    initializers,
    kind,
    isStatic,
    isPrivate,
    value,
    hasPrivateBrand,
  ) {
    var kindStr;

    switch (kind) {
      case 1 /* ACCESSOR */:
        kindStr = "accessor";
        break;
      case 2 /* METHOD */:
        kindStr = "method";
        break;
      case 3 /* GETTER */:
        kindStr = "getter";
        break;
      case 4 /* SETTER */:
        kindStr = "setter";
        break;
      default:
        kindStr = "field";
    }

    var ctx = {
      kind: kindStr,
      name: isPrivate ? "#" + name : toPropertyKey(name),
      static: isStatic,
      private: isPrivate,
    };

    var decoratorFinishedRef = { v: false };

    if (kind !== 0 /* FIELD */) {
      ctx.addInitializer = createAddInitializerMethod(
        initializers,
        decoratorFinishedRef,
      );
    }

    var get, set;
    if (!isPrivate && (kind === 0 /* FIELD */ || kind === 2) /* METHOD */) {
      get = function (target) {
        return target[name];
      };
      if (kind === 0 /* FIELD */) {
        set = function (target, v) {
          target[name] = v;
        };
      }
    } else if (kind === 2 /* METHOD */) {
      // Assert: isPrivate is true.
      get = function (target) {
        assertInstanceIfPrivate(hasPrivateBrand, target);
        return desc.value;
      };
    } else {
      // Assert: If kind === 0, then isPrivate is true.
      var t = kind === 0 /* FIELD */ || kind === 1; /* ACCESSOR */
      if (t || kind === 3 /* GETTER */) {
        if (isPrivate) {
          get = function (target) {
            assertInstanceIfPrivate(hasPrivateBrand, target);
            return desc.get.call(target);
          };
        } else {
          get = function (target) {
            return desc.get.call(target);
          };
        }
      }
      if (t || kind === 4 /* SETTER */) {
        if (isPrivate) {
          set = function (target, value) {
            assertInstanceIfPrivate(hasPrivateBrand, target);
            desc.set.call(target, value);
          };
        } else {
          set = function (target, value) {
            desc.set.call(target, value);
          };
        }
      }
    }
    var has = isPrivate
      ? hasPrivateBrand.bind()
      : function (target) {
          return name in target;
        };
    ctx.access =
      get && set
        ? { get: get, set: set, has: has }
        : get
          ? { get: get, has: has }
          : { set: set, has: has };

    try {
      return dec(value, ctx);
    } finally {
      decoratorFinishedRef.v = true;
    }
  }

  function assertNotFinished(decoratorFinishedRef, fnName) {
    if (decoratorFinishedRef.v) {
      throw new Error(
        "attempted to call " + fnName + " after decoration was finished",
      );
    }
  }

  function assertCallable(fn, hint) {
    if (typeof fn !== "function") {
      throw new TypeError(hint + " must be a function");
    }
  }

  function assertValidReturnValue(kind, value) {
    var type = typeof value;

    if (kind === 1 /* ACCESSOR */) {
      if (type !== "object" || value === null) {
        throw new TypeError(
          "accessor decorators must return an object with get, set, or init properties or void 0",
        );
      }
      if (value.get !== undefined) {
        assertCallable(value.get, "accessor.get");
      }
      if (value.set !== undefined) {
        assertCallable(value.set, "accessor.set");
      }
      if (value.init !== undefined) {
        assertCallable(value.init, "accessor.init");
      }
    } else if (type !== "function") {
      var hint;
      if (kind === 0 /* FIELD */) {
        hint = "field";
      } else if (kind === 10 /* CLASS */) {
        hint = "class";
      } else {
        hint = "method";
      }
      throw new TypeError(
        hint + " decorators must return a function or void 0",
      );
    }
  }

  function curryThis1(fn) {
    return function () {
      return fn(this);
    };
  }
  function curryThis2(fn) {
    return function (value) {
      fn(this, value);
    };
  }

  function applyMemberDec(
    ret,
    base,
    decInfo,
    name,
    kind,
    isStatic,
    isPrivate,
    initializers,
    hasPrivateBrand,
  ) {
    var decs = decInfo[0];

    var desc, init, prefix, value;

    if (isPrivate) {
      if (kind === 0 /* FIELD */ || kind === 1 /* ACCESSOR */) {
        desc = {
          get: curryThis1(decInfo[3]),
          set: curryThis2(decInfo[4]),
        };
        prefix = "get";
      } else {
        if (kind === 3 /* GETTER */) {
          desc = {
            get: decInfo[3],
          };
          prefix = "get";
        } else if (kind === 4 /* SETTER */) {
          desc = {
            set: decInfo[3],
          };
          prefix = "set";
        } else {
          desc = {
            value: decInfo[3],
          };
        }
      }
      if (kind !== 0 /* FIELD */) {
        if (kind === 1 /* ACCESSOR */) {
          setFunctionName(desc.set, "#" + name, "set");
        }
        setFunctionName(desc[prefix || "value"], "#" + name, prefix);
      }
    } else if (kind !== 0 /* FIELD */) {
      desc = Object.getOwnPropertyDescriptor(base, name);
    }

    if (kind === 1 /* ACCESSOR */) {
      value = {
        get: desc.get,
        set: desc.set,
      };
    } else if (kind === 2 /* METHOD */) {
      value = desc.value;
    } else if (kind === 3 /* GETTER */) {
      value = desc.get;
    } else if (kind === 4 /* SETTER */) {
      value = desc.set;
    }

    var newValue, get, set;

    if (typeof decs === "function") {
      newValue = memberDec(
        decs,
        name,
        desc,
        initializers,
        kind,
        isStatic,
        isPrivate,
        value,
        hasPrivateBrand,
      );

      if (newValue !== void 0) {
        assertValidReturnValue(kind, newValue);

        if (kind === 0 /* FIELD */) {
          init = newValue;
        } else if (kind === 1 /* ACCESSOR */) {
          init = newValue.init;
          get = newValue.get || value.get;
          set = newValue.set || value.set;

          value = { get: get, set: set };
        } else {
          value = newValue;
        }
      }
    } else {
      for (var i = decs.length - 1; i >= 0; i--) {
        var dec = decs[i];

        newValue = memberDec(
          dec,
          name,
          desc,
          initializers,
          kind,
          isStatic,
          isPrivate,
          value,
          hasPrivateBrand,
        );

        if (newValue !== void 0) {
          assertValidReturnValue(kind, newValue);
          var newInit;

          if (kind === 0 /* FIELD */) {
            newInit = newValue;
          } else if (kind === 1 /* ACCESSOR */) {
            newInit = newValue.init;
            get = newValue.get || value.get;
            set = newValue.set || value.set;

            value = { get: get, set: set };
          } else {
            value = newValue;
          }

          if (newInit !== void 0) {
            if (init === void 0) {
              init = newInit;
            } else if (typeof init === "function") {
              init = [init, newInit];
            } else {
              init.push(newInit);
            }
          }
        }
      }
    }

    if (kind === 0 /* FIELD */ || kind === 1 /* ACCESSOR */) {
      if (init === void 0) {
        // If the initializer was void 0, sub in a dummy initializer
        init = function (instance, init) {
          return init;
        };
      } else if (typeof init !== "function") {
        var ownInitializers = init;

        init = function (instance, init) {
          var value = init;

          for (var i = 0; i < ownInitializers.length; i++) {
            value = ownInitializers[i].call(instance, value);
          }

          return value;
        };
      } else {
        var originalInitializer = init;

        init = function (instance, init) {
          return originalInitializer.call(instance, init);
        };
      }

      ret.push(init);
    }

    if (kind !== 0 /* FIELD */) {
      if (kind === 1 /* ACCESSOR */) {
        desc.get = value.get;
        desc.set = value.set;
      } else if (kind === 2 /* METHOD */) {
        desc.value = value;
      } else if (kind === 3 /* GETTER */) {
        desc.get = value;
      } else if (kind === 4 /* SETTER */) {
        desc.set = value;
      }

      if (isPrivate) {
        if (kind === 1 /* ACCESSOR */) {
          ret.push(function (instance, args) {
            return value.get.call(instance, args);
          });
          ret.push(function (instance, args) {
            return value.set.call(instance, args);
          });
        } else if (kind === 2 /* METHOD */) {
          ret.push(value);
        } else {
          ret.push(function (instance, args) {
            return value.call(instance, args);
          });
        }
      } else {
        Object.defineProperty(base, name, desc);
      }
    }
  }

  function applyMemberDecs(Class, decInfos, instanceBrand) {
    var ret = [];
    var protoInitializers;
    var staticInitializers;
    var staticBrand;

    var existingProtoNonFields = new Map();
    var existingStaticNonFields = new Map();

    for (var i = 0; i < decInfos.length; i++) {
      var decInfo = decInfos[i];

      // skip computed property names
      if (!Array.isArray(decInfo)) continue;

      var kind = decInfo[1];
      var name = decInfo[2];
      var isPrivate = decInfo.length > 3;

      var isStatic = kind >= 5; /* STATIC */
      var base;
      var initializers;
      var hasPrivateBrand = instanceBrand;

      if (isStatic) {
        base = Class;
        kind = kind - 5 /* STATIC */;
        // initialize staticInitializers when we see a non-field static member
        if (kind !== 0 /* FIELD */) {
          staticInitializers = staticInitializers || [];
          initializers = staticInitializers;
        }
        if (isPrivate && !staticBrand) {
          staticBrand = function (_) {
            return checkInRHS(_) === Class;
          };
        }
        hasPrivateBrand = staticBrand;
      } else {
        base = Class.prototype;
        // initialize protoInitializers when we see a non-field member
        if (kind !== 0 /* FIELD */) {
          protoInitializers = protoInitializers || [];
          initializers = protoInitializers;
        }
      }

      if (kind !== 0 /* FIELD */ && !isPrivate) {
        var existingNonFields = isStatic
          ? existingStaticNonFields
          : existingProtoNonFields;

        var existingKind = existingNonFields.get(name) || 0;

        if (
          existingKind === true ||
          (existingKind === 3 /* GETTER */ && kind !== 4) /* SETTER */ ||
          (existingKind === 4 /* SETTER */ && kind !== 3) /* GETTER */
        ) {
          throw new Error(
            "Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " +
              name,
          );
        } else if (!existingKind && kind > 2 /* METHOD */) {
          existingNonFields.set(name, kind);
        } else {
          existingNonFields.set(name, true);
        }
      }

      applyMemberDec(
        ret,
        base,
        decInfo,
        name,
        kind,
        isStatic,
        isPrivate,
        initializers,
        hasPrivateBrand,
      );
    }

    pushInitializers(ret, protoInitializers);
    pushInitializers(ret, staticInitializers);
    return ret;
  }

  function pushInitializers(ret, initializers) {
    if (initializers) {
      ret.push(function (instance) {
        for (var i = 0; i < initializers.length; i++) {
          initializers[i].call(instance);
        }
        return instance;
      });
    }
  }

  function applyClassDecs(targetClass, classDecs) {
    if (classDecs.length > 0) {
      var initializers = [];
      var newClass = targetClass;
      var name = targetClass.name;

      for (var i = classDecs.length - 1; i >= 0; i--) {
        var decoratorFinishedRef = { v: false };

        try {
          var nextNewClass = classDecs[i](newClass, {
            kind: "class",
            name: name,
            addInitializer: createAddInitializerMethod(
              initializers,
              decoratorFinishedRef,
            ),
          });
        } finally {
          decoratorFinishedRef.v = true;
        }

        if (nextNewClass !== undefined) {
          assertValidReturnValue(10 /* CLASS */, nextNewClass);
          newClass = nextNewClass;
        }
      }

      return [
        newClass,
        function () {
          for (var i = 0; i < initializers.length; i++) {
            initializers[i].call(newClass);
          }
        },
      ];
    }
    // The transformer will not emit assignment when there are no class decorators,
    // so we don't have to return an empty array here.
  }

  /**
    Basic usage:

    applyDecs(
      Class,
      [
        // member decorators
        [
          dec,                // dec or array of decs
          0,                  // kind of value being decorated
          'prop',             // name of public prop on class containing the value being decorated,
          '#p',               // the name of the private property (if is private, void 0 otherwise),
        ]
      ],
      [
        // class decorators
        dec1, dec2
      ]
    )
    ```

    Fully transpiled example:

    ```js
    @dec
    class Class {
      @dec
      a = 123;

      @dec
      #a = 123;

      @dec
      @dec2
      accessor b = 123;

      @dec
      accessor #b = 123;

      @dec
      c() { console.log('c'); }

      @dec
      #c() { console.log('privC'); }

      @dec
      get d() { console.log('d'); }

      @dec
      get #d() { console.log('privD'); }

      @dec
      set e(v) { console.log('e'); }

      @dec
      set #e(v) { console.log('privE'); }
    }


    // becomes
    let initializeInstance;
    let initializeClass;

    let initA;
    let initPrivA;

    let initB;
    let initPrivB, getPrivB, setPrivB;

    let privC;
    let privD;
    let privE;

    let Class;
    class _Class {
      static {
        let ret = applyDecs(
          this,
          [
            [dec, 0, 'a'],
            [dec, 0, 'a', (i) => i.#a, (i, v) => i.#a = v],
            [[dec, dec2], 1, 'b'],
            [dec, 1, 'b', (i) => i.#privBData, (i, v) => i.#privBData = v],
            [dec, 2, 'c'],
            [dec, 2, 'c', () => console.log('privC')],
            [dec, 3, 'd'],
            [dec, 3, 'd', () => console.log('privD')],
            [dec, 4, 'e'],
            [dec, 4, 'e', () => console.log('privE')],
          ],
          [
            dec
          ]
        )

        initA = ret[0];

        initPrivA = ret[1];

        initB = ret[2];

        initPrivB = ret[3];
        getPrivB = ret[4];
        setPrivB = ret[5];

        privC = ret[6];

        privD = ret[7];

        privE = ret[8];

        initializeInstance = ret[9];

        Class = ret[10]

        initializeClass = ret[11];
      }

      a = (initializeInstance(this), initA(this, 123));

      #a = initPrivA(this, 123);

      #bData = initB(this, 123);
      get b() { return this.#bData }
      set b(v) { this.#bData = v }

      #privBData = initPrivB(this, 123);
      get #b() { return getPrivB(this); }
      set #b(v) { setPrivB(this, v); }

      c() { console.log('c'); }

      #c(...args) { return privC(this, ...args) }

      get d() { console.log('d'); }

      get #d() { return privD(this); }

      set e(v) { console.log('e'); }

      set #e(v) { privE(this, v); }
    }

    initializeClass(Class);
  */
  return function applyDecs2301(
    targetClass,
    memberDecs,
    classDecs,
    instanceBrand,
  ) {
    return {
      e: applyMemberDecs(targetClass, memberDecs, instanceBrand),
      // Lazily apply class decorations so that member init locals can be properly bound.
      get c() {
        return applyClassDecs(targetClass, classDecs);
      },
    };
  };
}

export default function applyDecs2301(
  targetClass,
  memberDecs,
  classDecs,
  instanceBrand,
) {
  return (applyDecs2301 = applyDecs2301Factory())(
    targetClass,
    memberDecs,
    classDecs,
    instanceBrand,
  );
}
